package org.song.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.song.cache.UserAccessCache;
import org.song.constant.Deleted;
import org.song.exception.myException.CommonException;
import org.song.mapper.BlogMapper;
import org.song.mapper.UserBlogOperateMapper;
import org.song.mapper.UserMapper;
import org.song.model.dto.BlogDto;
import org.song.model.dto.UserDto;
import org.song.model.entity.UserBlogOperateEntity;
import org.song.model.threadlocal.WebContextUtil;
import org.song.model.vo.Blog;
import org.song.model.vo.User;
import org.song.model.vo.common.BaseVo;
import org.song.service.UserService;
import org.song.service.cache.UserBlogOperateCache;
import org.song.service.impl.common.AbstractBaseServiceImpl;
import org.song.util.FunctionUtil;
import org.song.util.JacksonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Slf4j
@Service( value = "userServiceImpl")
public class UserServiceImpl extends AbstractBaseServiceImpl implements UserService {

    @Autowired( required = true )
    @Qualifier( value = "userMapper")
    private UserMapper userMapper;

    @Autowired( required = true )
    @Qualifier( value = "userBlogOperateMapper" )
    private UserBlogOperateMapper userBlogOperateMapper;

    @Autowired( required = true )
    @Qualifier( value = "userBlogOperateCacheImpl")
    private UserBlogOperateCache userBlogOperateCache;

    @Autowired( required = true )
    @Qualifier( value = "blogMapper")
    private BlogMapper blogMapper;

    @Autowired( required = true )
    @Qualifier( value = "userAccessCacheImpl")
    private UserAccessCache userAccessCache;

    @Override
    public List<User> listUsers( @NonNull UserDto userDto) {
        return userMapper.listUsers(userDto);
    }


    @Override
    public Integer deleteUser( @NonNull User user) {
        return userMapper.deleteUser(user);
    }

    @Override
    public Integer insertUser( @NonNull User user) {
        return userMapper.insertUser(user);
    }

    @Override
    public Integer updateUser( @NonNull User user) {
        return userMapper.updateUser(user);
    }

    /**
     * 添加用户博客操作记录
     * @param userBlogOperateEntity
     *  注意： 用户操作 缓存更新 DB更新 博客数据更新 一个接口保证原子性
     * @return
     */
    @Transactional( rollbackFor = { Exception.class })
    @NonNull
    @Override
    public Integer insertUserBlogOperate( @NonNull UserBlogOperateEntity userBlogOperateEntity) {
        // 未登录的用户使用IP地址作为用户Id
        this.updateUserLoginInfo( userBlogOperateEntity );
        // 操作纪律缓存更新
        userBlogOperateCache.insertUserBlogOperateInfo(
                userBlogOperateEntity.getOperateType() ,
                userBlogOperateEntity.getUserId() ,
                userBlogOperateEntity.getBlogId() ,
                null );
        //操作记录数据库
        userBlogOperateEntity.setId( userBlogOperateMapper.makeLongKey() );
        final int count = userBlogOperateMapper.insertSelective(userBlogOperateEntity);
        //更新博客数据
        //取出当前博客
        final BlogDto blogDto = BlogDto.builder().id( userBlogOperateEntity.getBlogId() ).build();
        Blog currentBlog = CollectionUtils.firstElement(blogMapper.listBlogs(blogDto));
        Assert.isTrue(Objects.nonNull( currentBlog ) && Objects.nonNull( currentBlog.getVersion() ) && Objects.nonNull( currentBlog.getId() )
                , () -> new CommonException( "当前博客已被删除~" ));
        // 判断当前用户操作类型
        if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.CLICK.getCode() ) ) {
            currentBlog.setViews( currentBlog.getViews() + 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.COMMENT.getCode() ) ) {
            currentBlog.setCommentCount( currentBlog.getCommentCount() + 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.THUMB.getCode() ) ) {
            currentBlog.setThumbs( currentBlog.getThumbs() + 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.COLLECT.getCode() ) ) {
            currentBlog.setCollectCount( currentBlog.getCollectCount() + 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.APPRECIATE.getCode() ) ) {
            currentBlog.setRewardCount( currentBlog.getRewardCount() + 1 );
        } else {
            // 发送告警
            log.error("当前用户操作类型异常，博客数据未更新。\n" + JacksonUtil.getJsonString( userBlogOperateEntity ) );
        }
        // （分布式场景使用）乐观锁 + 自旋锁 更新数据  注意：并发大的场景可能会造成线程阻塞较久 每次乐观锁更新失败 需要从DB查询最新数据
        //   建议改为 互斥锁 synchronized 分布式 场景下建议是用 redis 分布式锁 （redission（加锁解锁原子、可重入、watchDog） + 哨兵）
        while ( !blogMapper.updateBlogWithVersion( currentBlog ) ) {
            // 乐观锁更新失败自旋查询最新博客数据
            currentBlog = CollectionUtils.firstElement(blogMapper.listBlogs(blogDto));
        }
        return count;
    }

    /**
     * 获取当前用户指定博客指定类型的操作次数
     * @param userBlogOperateEntity
     * @return
     */
    @NonNull
    @Override
    public Integer getUserBlogOperate( @NonNull UserBlogOperateEntity userBlogOperateEntity ) {
        // 未登录的用户使用IP地址作为用户Id
        this.updateUserLoginInfo( userBlogOperateEntity );
        Assert.isTrue( Objects.nonNull( userBlogOperateEntity.getUserId() ) &&
                Objects.nonNull( userBlogOperateEntity.getBlogId() ) &&
                Objects.nonNull( userBlogOperateEntity.getOperateType() ) , () -> {
            log.error("获取当前用户博客操作记录失败： \n" + JacksonUtil.getJsonString( userBlogOperateEntity ));
            return new CommonException();
        }  );
        // 从缓存中获取用户操作记录次数
        Integer operateCount = userBlogOperateCache.getUserBlogOperateCache(
                userBlogOperateEntity.getOperateType() ,
                userBlogOperateEntity.getUserId() ,
                userBlogOperateEntity.getBlogId() );
         operateCount = Optional.ofNullable( operateCount ).orElseGet(() -> {
            //缓存为空从DB获取 TODO： 待做缓存穿透
            final Example example = new Example(UserBlogOperateEntity.class);
            example.createCriteria().andEqualTo("userId", userBlogOperateEntity.getUserId())
                    .andEqualTo("blogId", userBlogOperateEntity.getBlogId())
                    .andEqualTo("operateType", userBlogOperateEntity.getOperateType())
                    .andEqualTo("deleted", Deleted.NO.getCode());
            // 互斥锁（单机使用synchronized 分布式使用分布式锁保证互斥性 ） 高并发场景 缓存传统 防止缓存击穿
            synchronized (this) {
                final List<UserBlogOperateEntity> userBlogOperateEntityList = userBlogOperateMapper.selectByExample(example);
                if (!CollectionUtils.isEmpty(userBlogOperateEntityList)) {
                    log.error("当前用户操作记录，缓存不存在，数据库存在： " + JacksonUtil.getJsonString(userBlogOperateEntity));
                    // 写回缓存 （DB存储是操作记录 缓存的hashValue取条数即可）
                    userBlogOperateCache.insertUserBlogOperateInfo(userBlogOperateEntity.getOperateType()
                            , userBlogOperateEntity.getUserId(), userBlogOperateEntity.getBlogId(), userBlogOperateEntityList.size());
                }
                return userBlogOperateEntityList.size();
            }
        });
         return operateCount;
    }

    /**
     * 删除用户博客操作记录
     * @param userBlogOperateEntity
     * @return
     */
    @Nullable
    @Override
    public Integer deleteUserBlogOperate( @NonNull UserBlogOperateEntity userBlogOperateEntity) {
        // 未登录的用户使用IP地址作为用户Id
        this.updateUserLoginInfo( userBlogOperateEntity );
        Assert.isTrue( Objects.nonNull( userBlogOperateEntity.getUserId() ) &&
                Objects.nonNull( userBlogOperateEntity.getBlogId() ) &&
                Objects.nonNull( userBlogOperateEntity.getOperateType() ) , () -> {
            log.error("删除当前用户博客操作记录失败： \n" + JacksonUtil.getJsonString( userBlogOperateEntity ));
            return new CommonException();
        }  );
        if ( userBlogOperateCache.deleteUserBlogOperate(
                userBlogOperateEntity.getOperateType() ,
                userBlogOperateEntity.getUserId() ,
                userBlogOperateEntity.getBlogId()  ) ) {
            log.info("用户删除了指定博客操作行为：" + JacksonUtil.getJsonString( userBlogOperateEntity ));
        }
        // 删除DB数据
        final Example example = new Example( UserBlogOperateEntity.class );
        example.createCriteria().andEqualTo("userId", userBlogOperateEntity.getUserId())
                .andEqualTo("blogId", userBlogOperateEntity.getBlogId())
                .andEqualTo("operateType", userBlogOperateEntity.getOperateType())
                .andEqualTo("deleted", Deleted.NO.getCode());
        final UserBlogOperateEntity entity = new UserBlogOperateEntity();
        entity.setDeleted( Deleted.YES.getCode() );
        final Integer count = userBlogOperateMapper.updateByExampleSelective(entity, example);
        //更新博客数据
        //取出当前博客
        final BlogDto blogDto = BlogDto.builder().id( userBlogOperateEntity.getBlogId() ).build();
        Blog currentBlog = CollectionUtils.firstElement(blogMapper.listBlogs(blogDto));
        Assert.isTrue(Objects.nonNull( currentBlog ) && Objects.nonNull( currentBlog.getVersion() ) && Objects.nonNull( currentBlog.getId() )
                , () -> new CommonException( "当前博客已被删除~" ));
        // 判断当前用户操作类型
        if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.CLICK.getCode() ) ) {
            currentBlog.setViews( currentBlog.getViews() - 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.COMMENT.getCode() ) ) {
            currentBlog.setCommentCount( currentBlog.getCommentCount() - 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.THUMB.getCode() ) ) {
            currentBlog.setThumbs( currentBlog.getThumbs() - 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.COLLECT.getCode() ) ) {
            currentBlog.setCollectCount( currentBlog.getCollectCount() - 1 );
        } else if ( Objects.equals( userBlogOperateEntity.getOperateType() , UserBlogOperateEntity.UserBlogOperateEnum.APPRECIATE.getCode() ) ) {
            currentBlog.setRewardCount( currentBlog.getRewardCount() - 1 );
        } else {
            // 发送告警
            log.error("当前用户操作类型异常，博客数据未更新。\n" + JacksonUtil.getJsonString( userBlogOperateEntity ) );
        }
        // （分布式场景使用）乐观锁 + 自旋锁 更新数据  注意：并发大的场景可能会造成线程阻塞较久 每次乐观锁更新失败 需要从DB查询最新数据
        //   建议改为 互斥锁 synchronized 分布式 场景下建议是用 redis 分布式锁 （redission（加锁解锁原子、可重入、watchDog） + 哨兵）
        while ( !blogMapper.updateBlogWithVersion( currentBlog ) ) {
            // 乐观锁更新失败自旋查询最新博客数据
            currentBlog = CollectionUtils.firstElement(blogMapper.listBlogs(blogDto));
        }
        return count;
    }

    /**]
     * 潘顿用户是否是首次访问
     * @return
     */
    @Override
    public Boolean firstVisit() {
        // 获取用户的客户端IP
        final String ipAddress = FunctionUtil.getIpAddress(WebContextUtil.getCurrentRequest());
        Assert.isTrue( !StringUtils.isEmpty( ipAddress ) , () -> {
            log.info("用户开启了防火墙无法获取客户端IP");
            return new CommonException("系统开了点小差~");
        });
        final String userInfo = userAccessCache.getUserInfo(ipAddress);
        if ( !StringUtils.isEmpty( userInfo ) ) {
            // 用户不是第一次访问
            return true;
        }
        // 用户第一次访问访问IP落库
        // 用户第一次访问网站存入缓存
        userAccessCache.setUerInfo( ipAddress , DateUtil.format( new Date() , BaseVo.TIME_FORMAT_PATTERN) );
        log.info("新用户访问\n" + "ip: " + ipAddress);
        return false;
    }


}
